package edu.northwestern.cbits.purple_robot_manager.probes.builtin; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.support.v4.content.ContextCompat; import android.telephony.PhoneNumberUtils; import edu.northwestern.cbits.purple_robot_manager.EncryptionManager; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.activities.probes.AddressBookLabelActivity; import edu.northwestern.cbits.purple_robot_manager.activities.settings.FlexibleListPreference; import edu.northwestern.cbits.purple_robot_manager.calibration.ContactCalibrationHelper; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.logging.SanityManager; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; public class CommunicationEventProbe extends Probe { private static final String TYPE = "COMMUNICATION_TYPE"; private static final String TYPE_PHONE = "PHONE"; private static final String TYPE_SMS = "SMS"; private static final String DIRECTION = "COMMUNICATION_DIRECTION"; private static final String INCOMING = "INCOMING"; private static final String OUTGOING = "OUTGOING"; private static final String MISSED = "MISSED"; private static final String UNKNOWN = "UNKNOWN"; private static final String GROUP = "GROUP"; private static final String NUMBER = "NUMBER"; private static final String NAME = "NAME"; private static final String TIMESTAMP = "COMM_TIMESTAMP"; private static final String DURATION = "DURATION"; private static final String MESSAGE_BODY = "MESSAGE_BODY"; private static final String NORMALIZED_HASH = "NORMALIZED_HASH"; public static final boolean DEFAULT_ENABLED = true; private static final boolean DEFAULT_RETRIEVE = false; private static final boolean DEFAULT_ENCRYPT = true; public static final String ENABLED = "config_probe_communication_event_enabled"; private static final String FREQUENCY = "config_probe_communication_event_frequency"; private static final String HASH_DATA = "config_probe_communication_event_hash_data"; private static final String RECENT_EVENT = "config_probe_communication_event_recent"; private static final String RETRIEVE_DATA = "config_probe_communication_event_retrieve_data"; private static final String ENCRYPT_DATA = "config_probe_communication_event_encrypt_data"; public static final boolean DEFAULT_ENABLE_CALIBRATION_NOTIFICATIONS = true; public static final String ENABLE_CALIBRATION_NOTIFICATIONS = "config_probe_communication_event_calibration_notifications"; private long _lastCheck = 0; @Override public String getPreferenceKey() { return "built_in_communication_event"; } @Override public String name(Context context) { return "edu.northwestern.cbits.purple_robot_manager.probes.builtin.CommunicationEventProbe"; } @Override public String title(Context context) { return context.getString(R.string.title_communication_event_probe); } @Override public String probeCategory(Context context) { return context.getResources().getString(R.string.probe_personal_info_category); } @Override public void enable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(CommunicationEventProbe.ENABLED, true); e.commit(); } @Override public void disable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(CommunicationEventProbe.ENABLED, false); e.commit(); } @Override @SuppressWarnings("deprecation") public boolean isEnabled(Context context) { SharedPreferences prefs = Probe.getPreferences(context); if (super.isEnabled(context)) { long now = System.currentTimeMillis(); if (prefs.getBoolean(CommunicationEventProbe.ENABLED, CommunicationEventProbe.DEFAULT_ENABLED)) { boolean ready = true; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (ContextCompat.checkSelfPermission(context, "android.permission.READ_CALL_LOG") != PackageManager.PERMISSION_GRANTED) { SanityManager.getInstance(context).addPermissionAlert(this.name(context), "android.permission.READ_CALL_LOG", context.getString(R.string.rationale_event_call_log_probe), null); ready = false; } if (ContextCompat.checkSelfPermission(context, "android.permission.READ_SMS") != PackageManager.PERMISSION_GRANTED) { SanityManager.getInstance(context).addPermissionAlert(this.name(context), "android.permission.READ_SMS", context.getString(R.string.rationale_event_sms_log_probe), null); ready = false; } } if (ready) { SanityManager.getInstance(context).clearPermissionAlert("android.permission.READ_CALL_LOG"); SanityManager.getInstance(context).clearPermissionAlert("android.permission.READ_SMS"); synchronized (this) { long freq = Long.parseLong(prefs.getString(CommunicationEventProbe.FREQUENCY, Probe.DEFAULT_FREQUENCY)); boolean doHash = prefs.getBoolean(CommunicationEventProbe.HASH_DATA, Probe.DEFAULT_HASH_DATA); if (now - this._lastCheck > freq) { long mostRecent = prefs.getLong(CommunicationEventProbe.RECENT_EVENT, 0); long newRecent = mostRecent; try { EncryptionManager em = EncryptionManager.getInstance(); String selection = "date > ?"; String[] args = {"" + mostRecent}; Cursor c = context.getContentResolver().query(CallLog.Calls.CONTENT_URI, null, selection, args, "date"); while (c.moveToNext()) { Bundle bundle = new Bundle(); bundle.putString("PROBE", this.name(context)); bundle.putLong("TIMESTAMP", System.currentTimeMillis() / 1000); bundle.putString(CommunicationEventProbe.TYPE, CommunicationEventProbe.TYPE_PHONE); String numberName = c.getString(c.getColumnIndex(Calls.CACHED_NAME)); String phoneNumber = PhoneNumberUtils.formatNumber(c.getString(c.getColumnIndex(Calls.NUMBER))); if (numberName == null) numberName = phoneNumber; String group = ContactCalibrationHelper.getGroup(context, numberName, false); if (group == null) group = ContactCalibrationHelper.getGroup(context, phoneNumber, true); if (group != null) bundle.putString(CommunicationEventProbe.GROUP, group); bundle.putString(CommunicationEventProbe.NORMALIZED_HASH, EncryptionManager.normalizedPhoneHash(context, phoneNumber)); if (doHash) { numberName = em.createHash(context, numberName); phoneNumber = em.createHash(context, phoneNumber); } bundle.putString(CommunicationEventProbe.NAME, numberName); bundle.putString(CommunicationEventProbe.NUMBER, phoneNumber); long callTime = c.getLong(c.getColumnIndex(Calls.DATE)); bundle.putLong(CommunicationEventProbe.TIMESTAMP, callTime); if (callTime > newRecent) newRecent = callTime; bundle.putLong(CommunicationEventProbe.DURATION, c.getLong(c.getColumnIndex(Calls.DURATION))); int callType = c.getInt(c.getColumnIndex(Calls.TYPE)); if (callType == Calls.OUTGOING_TYPE) bundle.putString(CommunicationEventProbe.DIRECTION, CommunicationEventProbe.OUTGOING); else if (callType == Calls.INCOMING_TYPE) bundle.putString(CommunicationEventProbe.DIRECTION, CommunicationEventProbe.INCOMING); else if (callType == Calls.MISSED_TYPE) bundle.putString(CommunicationEventProbe.DIRECTION, CommunicationEventProbe.MISSED); else bundle.putString(CommunicationEventProbe.DIRECTION, CommunicationEventProbe.UNKNOWN); this.transmitData(context, bundle); } c.close(); c = context.getContentResolver().query(Uri.parse("content://sms/inbox"), null, selection, args, "date"); while (c.moveToNext()) { Bundle bundle = new Bundle(); bundle.putString("PROBE", this.name(context)); bundle.putLong("TIMESTAMP", System.currentTimeMillis() / 1000); bundle.putString(CommunicationEventProbe.TYPE, CommunicationEventProbe.TYPE_SMS); String numberName = c.getString(c.getColumnIndex("person")); String phoneNumber = PhoneNumberUtils.formatNumber(c.getString(c.getColumnIndex("address"))); if (numberName == null) numberName = phoneNumber; String group = ContactCalibrationHelper.getGroup(context, numberName, false); if (group == null) group = ContactCalibrationHelper.getGroup(context, phoneNumber, true); if (group != null) bundle.putString(CommunicationEventProbe.GROUP, group); if (doHash) { numberName = em.createHash(context, numberName); phoneNumber = em.createHash(context, phoneNumber); } bundle.putString(CommunicationEventProbe.NAME, numberName); bundle.putString(CommunicationEventProbe.NUMBER, phoneNumber); bundle.putString(CommunicationEventProbe.NORMALIZED_HASH, EncryptionManager.normalizedPhoneHash(context, phoneNumber)); long callTime = c.getLong(c.getColumnIndex("date")); bundle.putLong(CommunicationEventProbe.TIMESTAMP, callTime); if (callTime > newRecent) newRecent = callTime; bundle.putString(CommunicationEventProbe.DIRECTION, CommunicationEventProbe.INCOMING); boolean retrieve = prefs.getBoolean(CommunicationEventProbe.RETRIEVE_DATA, CommunicationEventProbe.DEFAULT_RETRIEVE); boolean encrypt = prefs.getBoolean(CommunicationEventProbe.ENCRYPT_DATA, CommunicationEventProbe.DEFAULT_ENCRYPT); if (retrieve) { String body = c.getString(c.getColumnIndex("body")); if (encrypt) body = em.encryptString(context, body); bundle.putString(CommunicationEventProbe.MESSAGE_BODY, body); } this.transmitData(context, bundle); } c.close(); c = context.getContentResolver().query(Uri.parse("content://sms/sent"), null, selection, args, "date"); while (c.moveToNext()) { Bundle bundle = new Bundle(); bundle.putString("PROBE", this.name(context)); bundle.putLong("TIMESTAMP", System.currentTimeMillis() / 1000); bundle.putString(CommunicationEventProbe.TYPE, CommunicationEventProbe.TYPE_SMS); String numberName = c.getString(c.getColumnIndex("person")); String phoneNumber = PhoneNumberUtils.formatNumber(c.getString(c.getColumnIndex("address"))); bundle.putString(CommunicationEventProbe.NORMALIZED_HASH, EncryptionManager.normalizedPhoneHash(context, phoneNumber)); if (numberName == null) numberName = phoneNumber; String group = ContactCalibrationHelper.getGroup(context, numberName, false); if (group == null) group = ContactCalibrationHelper.getGroup(context, phoneNumber, true); if (group != null) bundle.putString(CommunicationEventProbe.GROUP, group); if (doHash) { numberName = em.createHash(context, numberName); phoneNumber = em.createHash(context, phoneNumber); } bundle.putString(CommunicationEventProbe.NAME, numberName); bundle.putString(CommunicationEventProbe.NUMBER, phoneNumber); long callTime = c.getLong(c.getColumnIndex("date")); bundle.putLong(CommunicationEventProbe.TIMESTAMP, callTime); if (callTime > newRecent) newRecent = callTime; bundle.putString(CommunicationEventProbe.DIRECTION, CommunicationEventProbe.OUTGOING); boolean retrieve = prefs.getBoolean(CommunicationEventProbe.RETRIEVE_DATA, CommunicationEventProbe.DEFAULT_RETRIEVE); boolean encrypt = prefs.getBoolean(CommunicationEventProbe.ENCRYPT_DATA, CommunicationEventProbe.DEFAULT_ENCRYPT); if (retrieve) { String body = c.getString(c.getColumnIndex("body")); if (encrypt) body = em.encryptString(context, body); bundle.putString(CommunicationEventProbe.MESSAGE_BODY, body); } this.transmitData(context, bundle); } c.close(); } catch (Exception e) { // Broken call & SMS databases on several devices... // Ignoring. LogManager.getInstance(context).logException(e); } this._lastCheck = now; Editor e = prefs.edit(); e.putLong(CommunicationEventProbe.RECENT_EVENT, newRecent); e.commit(); } } } return true; } } return false; } @Override @SuppressLint("SimpleDateFormat") public String summarizeValue(Context context, Bundle bundle) { String name = bundle.getString(CommunicationEventProbe.NAME); String type = bundle.getString(CommunicationEventProbe.TYPE); if (CommunicationEventProbe.TYPE_PHONE.equals(type)) type = context.getResources().getString(R.string.summary_communication_events_phone_type); else type = context.getResources().getString(R.string.summary_communication_events_sms_type); long timestamp = (long) bundle.getDouble(CommunicationEventProbe.TIMESTAMP); SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy 'at' HH:mm"); return String.format(context.getResources().getString(R.string.summary_communication_events_probe), type, name, format.format(new Date(timestamp))); } @Override public Map<String, Object> configuration(Context context) { Map<String, Object> map = super.configuration(context); SharedPreferences prefs = Probe.getPreferences(context); long freq = Long.parseLong(prefs.getString(CommunicationEventProbe.FREQUENCY, Probe.DEFAULT_FREQUENCY)); map.put(Probe.PROBE_FREQUENCY, freq); boolean hash = prefs.getBoolean(CommunicationEventProbe.HASH_DATA, Probe.DEFAULT_HASH_DATA); map.put(Probe.HASH_DATA, hash); boolean encrypt = prefs.getBoolean(CommunicationEventProbe.ENCRYPT_DATA, CommunicationEventProbe.DEFAULT_ENCRYPT); map.put(CommunicationEventProbe.ENCRYPT_DATA, encrypt); boolean retrieve = prefs.getBoolean(CommunicationEventProbe.RETRIEVE_DATA, CommunicationEventProbe.DEFAULT_RETRIEVE); map.put(CommunicationEventProbe.RETRIEVE_DATA, retrieve); boolean calibrateNotes = prefs.getBoolean(CommunicationEventProbe.ENABLE_CALIBRATION_NOTIFICATIONS, CommunicationEventProbe.DEFAULT_ENABLE_CALIBRATION_NOTIFICATIONS); map.put(Probe.PROBE_CALIBRATION_NOTIFICATIONS, calibrateNotes); return map; } @Override public void updateFromMap(Context context, Map<String, Object> params) { super.updateFromMap(context, params); if (params.containsKey(Probe.PROBE_FREQUENCY)) { Object frequency = params.get(Probe.PROBE_FREQUENCY); if (frequency instanceof Double) { frequency = ((Double) frequency).longValue(); } if (frequency instanceof Long) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putString(CommunicationEventProbe.FREQUENCY, frequency.toString()); e.commit(); } } if (params.containsKey(Probe.HASH_DATA)) { Object hash = params.get(Probe.HASH_DATA); if (hash instanceof Boolean) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(CommunicationEventProbe.HASH_DATA, (Boolean) hash); e.commit(); } } if (params.containsKey(CommunicationEventProbe.RETRIEVE_DATA)) { Object retrieve = params.get(CommunicationEventProbe.RETRIEVE_DATA); if (retrieve instanceof Boolean) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(CommunicationEventProbe.RETRIEVE_DATA, (Boolean) retrieve); e.commit(); } } if (params.containsKey(CommunicationEventProbe.ENCRYPT_DATA)) { Object encrypt = params.get(CommunicationEventProbe.ENCRYPT_DATA); if (encrypt instanceof Boolean) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(CommunicationEventProbe.ENCRYPT_DATA, (Boolean) encrypt); e.commit(); } } if (params.containsKey(Probe.PROBE_CALIBRATION_NOTIFICATIONS)) { Object enable = params.get(Probe.PROBE_CALIBRATION_NOTIFICATIONS); if (enable instanceof Boolean) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(CommunicationEventProbe.ENABLE_CALIBRATION_NOTIFICATIONS, ((Boolean) enable)); e.commit(); } } } @Override public String summary(Context context) { return context.getString(R.string.summary_communication_event_probe_desc); } @Override @SuppressWarnings("deprecation") public PreferenceScreen preferenceScreen(final Context context, PreferenceManager manager) { PreferenceScreen screen = super.preferenceScreen(context, manager); screen.setTitle(this.title(context)); screen.setSummary(R.string.summary_communication_event_probe_desc); CheckBoxPreference enabled = new CheckBoxPreference(context); enabled.setTitle(R.string.title_enable_probe); enabled.setKey(CommunicationEventProbe.ENABLED); enabled.setDefaultValue(CommunicationEventProbe.DEFAULT_ENABLED); screen.addPreference(enabled); FlexibleListPreference duration = new FlexibleListPreference(context); duration.setKey(CommunicationEventProbe.FREQUENCY); duration.setEntryValues(R.array.probe_low_frequency_values); duration.setEntries(R.array.probe_low_frequency_labels); duration.setTitle(R.string.probe_frequency_label); duration.setDefaultValue(Probe.DEFAULT_FREQUENCY); screen.addPreference(duration); CheckBoxPreference hash = new CheckBoxPreference(context); hash.setKey(CommunicationEventProbe.HASH_DATA); hash.setDefaultValue(Probe.DEFAULT_HASH_DATA); hash.setTitle(R.string.config_probe_communication_hash_title); hash.setSummary(R.string.config_probe_communication_hash_summary); screen.addPreference(hash); CheckBoxPreference retrieve = new CheckBoxPreference(context); retrieve.setKey(CommunicationEventProbe.RETRIEVE_DATA); retrieve.setDefaultValue(CommunicationEventProbe.DEFAULT_RETRIEVE); retrieve.setTitle(R.string.config_probe_communication_retrieve_title); retrieve.setSummary(R.string.config_probe_communication_retrieve_summary); retrieve.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { @Override public boolean onPreferenceChange(Preference arg0, Object newValue) { Boolean b = (Boolean) newValue; if (b) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder = builder.setTitle(R.string.config_probe_communication_retrieve_warning_title); builder = builder.setMessage(R.string.config_probe_communication_retrieve_warning); builder = builder.setPositiveButton(R.string.button_continue, null); builder.create().show(); } return true; } }); screen.addPreference(retrieve); CheckBoxPreference encrypt = new CheckBoxPreference(context); encrypt.setKey(CommunicationEventProbe.ENCRYPT_DATA); encrypt.setDefaultValue(CommunicationEventProbe.DEFAULT_ENCRYPT); encrypt.setTitle(R.string.config_probe_communication_encrypt_title); encrypt.setSummary(R.string.config_probe_communication_encrypt_summary); screen.addPreference(encrypt); Preference calibrate = new Preference(context); calibrate.setTitle(R.string.config_probe_calibrate_title); calibrate.setOnPreferenceClickListener(new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference pref) { Intent intent = new Intent(context, AddressBookLabelActivity.class); context.startActivity(intent); return true; } }); screen.addPreference(calibrate); CheckBoxPreference enableCalibrationNotifications = new CheckBoxPreference(context); enableCalibrationNotifications.setTitle(R.string.title_enable_calibration_notifications); enableCalibrationNotifications.setSummary(R.string.summary_enable_calibration_notifications); enableCalibrationNotifications.setKey(CommunicationEventProbe.ENABLE_CALIBRATION_NOTIFICATIONS); enableCalibrationNotifications.setDefaultValue(CommunicationEventProbe.DEFAULT_ENABLE_CALIBRATION_NOTIFICATIONS); screen.addPreference(enableCalibrationNotifications); return screen; } @Override public JSONObject fetchSettings(Context context) { JSONObject settings = super.fetchSettings(context); try { JSONArray values = new JSONArray(); values.put(true); values.put(false); JSONObject enabled = new JSONObject(); enabled.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_BOOLEAN); enabled.put(Probe.PROBE_VALUES, values); settings.put(Probe.PROBE_CALIBRATION_NOTIFICATIONS, enabled); JSONObject encrypt = new JSONObject(); encrypt.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_BOOLEAN); encrypt.put(Probe.PROBE_VALUES, values); settings.put(CommunicationEventProbe.ENCRYPT_DATA, encrypt); JSONObject retrieve = new JSONObject(); retrieve.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_BOOLEAN); retrieve.put(Probe.PROBE_VALUES, values); settings.put(CommunicationEventProbe.RETRIEVE_DATA, retrieve); JSONObject hash = new JSONObject(); hash.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_BOOLEAN); hash.put(Probe.PROBE_VALUES, values); settings.put(Probe.HASH_DATA, hash); JSONObject frequency = new JSONObject(); frequency.put(Probe.PROBE_TYPE, Probe.PROBE_TYPE_LONG); values = new JSONArray(); String[] options = context.getResources().getStringArray(R.array.probe_low_frequency_values); for (String option : options) { values.put(Long.parseLong(option)); } frequency.put(Probe.PROBE_VALUES, values); settings.put(Probe.PROBE_FREQUENCY, frequency); } catch (JSONException e) { LogManager.getInstance(context).logException(e); } return settings; } }